﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using System.ComponentModel;

namespace FileMover.Service
{
    public interface IRuleItem
    {
        string DisplayString();
        XmlElement Xml { get; }
    }
    public interface IFilter : IRuleItem
    {
        void Filter(ILog Log, IItem item);

    }
    public interface IFilterFactory : IFactory
    {
        IFilter FromXml(XmlElement elem);
    }

    public interface IAction : IRuleItem
    {
        void Process(ILog Log, bool TestMode, List<IItem> items);
    }
    public interface IActionFactory : IFactory
    {
        IAction FromXml(XmlElement elem);
    }

    public enum LevelEnum
    {
        Debug,
        Info,
        Action,
        Error
    };

    public interface ILog
    {
        void Log(LevelEnum level, string format, params object[] formatParams);
    }
    public interface IFactory
    {
        bool Handles(XmlElement elem);

        IRuleItem New(XmlDocument xmlDoc);
    }
    public abstract class RuleItemBase : IRuleItem
    {
        protected XmlElement _elem;
        public virtual void FromXml(XmlElement elem)
        {
            _elem = elem;
        }

        [Browsable(false)]
        public XmlElement Xml
        {
            get
            {
                return _elem;
            }
        }

        #region IRuleItem Member

        public abstract string DisplayString();

        #endregion
    }


    public abstract class FilterFactoryBase<T> : FactoryBase<T>, IFilterFactory where T : RuleItemBase, IFilter, new()
    {
        IFilter IFilterFactory.FromXml(System.Xml.XmlElement elem)
        {
            if (!string.Equals(elem.NamespaceURI, XmlNameSpace))
                return null;
            if (!string.Equals(XmlLocalName, elem.LocalName))
                return null;

            var _retval = new T();
            _retval.FromXml(elem);
            return _retval;
        }
        public override IRuleItem New(XmlDocument xmlDoc)
        {
            var elem = xmlDoc.CreateElement(XmlLocalName, XmlNameSpace);
            return (this as IFilterFactory).FromXml(elem);
        }
    }

    public abstract class FactoryBase<T> : IFactory where T : RuleItemBase, new()
    {
        protected abstract string XmlLocalName { get; }
        protected abstract string XmlNameSpace { get; }


        #region IFactory Member

        bool IFactory.Handles(XmlElement elem)
        {
            if (!string.Equals(elem.NamespaceURI, XmlNameSpace))
                return false;
            if (!string.Equals(XmlLocalName, elem.LocalName))
                return false;

            return true;
        }

        public abstract IRuleItem New(XmlDocument doc);
        #endregion
    }


    public abstract class ActionFactoryBase<T> : FactoryBase<T>, IActionFactory where T : RuleItemBase, IAction, new()
    {
        IAction IActionFactory.FromXml(System.Xml.XmlElement elem)
        {
            if (!string.Equals(elem.NamespaceURI, XmlNameSpace))
                return null;
            if (!string.Equals(XmlLocalName, elem.LocalName))
                return null;

            var _retval = new T();
            _retval.FromXml(elem);
            return _retval;
        }
        public override IRuleItem New(XmlDocument xmlDoc)
        {
            var elem = xmlDoc.CreateElement(XmlLocalName, XmlNameSpace);
            return (this as IActionFactory).FromXml(elem);
        }

    }


    public class FactoryRegistry<T> where T : IFactory
    {
        private static readonly List<T> factories = new List<T>();
        public void Register(T factory)
        {
            factories.Add(factory);
        }
        public T GetFactory(XmlElement elem)
        {
            return factories.Find((factory) => factory.Handles(elem));
        }
        public System.Collections.ObjectModel.ReadOnlyCollection<T> GetFactories()
        {
            return new System.Collections.ObjectModel.ReadOnlyCollection<T>(factories);
        }
    }
    public static class Registry
    {
        private static List<T> FindFactories<T>(Assembly assembly)
        {
            var _retval = new List<T>();
            foreach (var type in assembly.GetExportedTypes())
            {
                var implements = type.FindInterfaces((_if, _dummy)
                =>
                {
                    return ((_if == typeof(T)) && (!type.ContainsGenericParameters));
                }, null);
                if (implements.Length > 0)
                {
                    var obj = Activator.CreateInstance(type);
                    _retval.Add((T)obj);
                }
            }
            return _retval;
        }

        public static void ScanAssembly(Assembly assembly)
        {
            FindFactories<IFilterFactory>(assembly).ForEach((item) => Filters.Register(item));
            FindFactories<IActionFactory>(assembly).ForEach((item) => Actions.Register(item));
        }
        public static void Scan()
        {
            ScanAssembly(typeof(Registry).Assembly);

        }
        public static readonly FactoryRegistry<IFilterFactory> Filters = new FactoryRegistry<IFilterFactory>();
        public static readonly FactoryRegistry<IActionFactory> Actions = new FactoryRegistry<IActionFactory>();
    }

    public static class ItemHelper
    {
        private static readonly Regex _regexMatchVar = new Regex(@"(\$([0-9]+)|\$\(([^)]+)\))");
        public static string ResolveVars(IItem item, string value, List<string> linearArguments)
        {
            return _regexMatchVar.Replace(value, (m) =>
            {
                if (m.Groups[2].Success && (linearArguments != null))
                {
                    Int32 index;
                    if (Int32.TryParse(m.Groups[2].Value, out index))
                    {
                        if (index < linearArguments.Count)
                        {
                            return linearArguments[index];
                        }
                    }
                }
                else if (m.Groups[3].Success)
                {
                    return item.GetProperty(m.Groups[3].Value);
                }
                return string.Empty;
            });
        }
    }

    public class ItemBase : IItem
    {
        private Dictionary<string, string> _values;
        public ItemBase()
        {
            Usage = UsageEnum.Unknown;
            Handled = false;
            _values = new Dictionary<string, string>();
        }
        #region IItem Member

        public UsageEnum Usage { get; set; }

        public bool Handled { get; set; }

        protected string FormatKey(string key)
        {
            return key.ToLower().Trim();
        }

        public string GetProperty(string key)
        {
            string _retval;
            if (!_values.TryGetValue(FormatKey(key), out _retval))
                return string.Empty;
            return _retval;
        }

        public void SetProperty(string key, string value)
        {
            _values[FormatKey(key)] = value;
        }

        #endregion
    }

    public class DirectoryItemProvider : IItemProvider
    {
        public string SourceDirectory { get; set; }

        public class FileItem : ItemBase
        {
            private FileInfo _fi;

            public const string EXT = "extension";
            public const string NAME = "filename";
            public const string FULLNAME = "fullname";

            internal FileItem(FileInfo fi)
            {
                _fi = fi;

                SetProperty(EXT, fi.Extension);
                SetProperty(NAME, Path.GetFileNameWithoutExtension(fi.Name));
                SetProperty(FULLNAME, fi.FullName);
            }

        }

        public enum DepthEnum
        {
            Files,
            Infinity
        }

        public DepthEnum Depth { get; set; }
        public readonly List<Rule> Rules = new List<Rule>();

        public List<IItem> GetItems()
        {
            var di = new DirectoryInfo(SourceDirectory);
            if (!di.Exists)
                return null;

            var _retval = new List<IItem>();
            foreach (var fi in di.GetFiles("*", Depth == DepthEnum.Files ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories))
            {
                _retval.Add(new FileItem(fi));
            }
            return _retval;
        }

        internal void FromXml(XmlElement parentElem)
        {
            SourceDirectory = parentElem.GetAttribute("source");
            Depth = string.Equals(parentElem.GetAttribute("depth"), "files") ? DepthEnum.Files : DepthEnum.Infinity;

            foreach (XmlElement elem in parentElem.SelectNodes("FileMover:Rule", Model.mgr))
            {
                var rule = new Rule();
                rule.FromXml(elem);
                Rules.Add(rule);
            }
        }

        internal void Process(ILog log, bool TestMode)
        {
            var items = GetItems();
            if ((items == null) || (items.Count == 0))
            {
                log.Log(LevelEnum.Info, "Directory '{0}' contains no files", SourceDirectory);
                return;
            }
            log.Log(LevelEnum.Debug, "There are {0} items to process", items.Count);
            for (var i = 0; i < items.Count; ++i)
            {
                log.Log(LevelEnum.Debug, "#{0}: {1}", i, items[i].GetProperty(FileItem.FULLNAME));
            }
            foreach (var rule in Rules)
            {
                rule.Process(log, TestMode, items);
            }
            var unhandledItems = items.FindAll((item) => !item.Handled);
            log.Log(LevelEnum.Debug, "There are still {0} unprocessed items", unhandledItems.Count);
            for (var i = 0; i < unhandledItems.Count; ++i)
            {
                log.Log(LevelEnum.Debug, "#{0}: {1}", i, unhandledItems[i].GetProperty(FileItem.FULLNAME));
            }

        }
    }


    public class Rule
    {
        public string Comment { get; set; }
        public readonly List<IRuleItem> FiltersAndActions = new List<IRuleItem>();

        public UsageEnum DefaultUsage;

        XmlElement _elem;
        internal void FromXml(XmlElement parentElem)
        {
            _elem = parentElem;
            DefaultUsage = UsageEnum.Exclude;
            Comment = parentElem.GetAttribute("comment");

            var outcome = parentElem.GetAttribute("defaultOutcome");
            if (string.IsNullOrEmpty(outcome))
            {
            }
            else if (string.Equals(outcome, "include"))
            {
                DefaultUsage = UsageEnum.Include;
            }
            else if (string.Equals(outcome, "exclude"))
            {
                DefaultUsage = UsageEnum.Exclude;
            }
            else
            {
                throw new XmlException(string.Format("Rule attribute defaultOutcome has an invalid value '{0}'", outcome));
            }
            foreach (XmlElement elem in parentElem.SelectNodes("*"))
            {
                var filterFactory = Registry.Filters.GetFactory(elem);
                if (filterFactory != null)
                {
                    var filter = filterFactory.FromXml(elem);
                    FiltersAndActions.Add(filter);
                    continue;

                }

                var actionFactory = Registry.Actions.GetFactory(elem);
                if (actionFactory != null)
                {
                    var action = actionFactory.FromXml(elem);
                    FiltersAndActions.Add(action);
                    continue;
                }

                throw new ArgumentException(string.Format("Invalid xmlNode '{0}' is not a registered filter or action!", elem.Name));
            }

        }


        internal void Process(ILog log, bool TestMode, List<IItem> items)
        {
            log.Log(LevelEnum.Info, "Processing Rule: {0}", Comment);
            var unhandledItems = items.FindAll((item) => !item.Handled);

            unhandledItems.ForEach((item) => item.Usage = DefaultUsage);

            foreach (var filterOrAction in this.FiltersAndActions)
            {
                if (filterOrAction is IFilter)
                {
                    unhandledItems.ForEach((item) =>
                            (filterOrAction as IFilter).Filter(log, item));
                }
                else if (filterOrAction is IAction)
                {
                    (filterOrAction as IAction).Process(log, TestMode, unhandledItems.FindAll((item) => item.Usage == UsageEnum.Include));
                    unhandledItems = items.FindAll((item) => !item.Handled);
                }
                else
                {
                }

            }
        }

        public IRuleItem AddFilterOrAction(IFactory factory)
        {
            if (factory == null)
                return null;
            if (_elem == null)
                return null;
            var action = factory.New(_elem.OwnerDocument);
            _elem.AppendChild(action.Xml);
            this.FiltersAndActions.Add(action);
            return action;
        }
    }

    public class Model
    {
        public readonly List<DirectoryItemProvider> Directories = new List<DirectoryItemProvider>();

        public static readonly XmlNamespaceManager mgr;

        public void Test(ILog log)
        {
            foreach (var dir in Directories)
            {
                dir.Process(log, true);
            }
        }
        static Model()
        {
            mgr = new XmlNamespaceManager(new NameTable());
            mgr.AddNamespace("FileMover", "http://FileMover.codeplex.com/v1.xsd");
        }
        public void Clear()
        {
            Directories.Clear();
        }
        private XmlDocument _xmlDoc;

        public void Read(XmlDocument xmlDoc)
        {
            if (xmlDoc == null)
                throw new ArgumentNullException("xmlDoc");

            if (xmlDoc.DocumentElement == null)
                throw new ArgumentException("xmlDoc has no documentElement");

            if (xmlDoc.DocumentElement.NamespaceURI != mgr.LookupNamespace("FileMover"))
                throw new ArgumentException("xmlDoc has an invalid format (wrong namespace?)");

            if (!string.Equals("FileMover", xmlDoc.DocumentElement.LocalName))
                throw new ArgumentException("xmlDoc has an invalid format (wrong root element?)");

            _xmlDoc = xmlDoc;
            foreach (XmlElement elem in xmlDoc.DocumentElement.SelectNodes("FileMover:Directory", mgr))
            {
                var dir = new DirectoryItemProvider();
                dir.FromXml(elem);
                Directories.Add(dir);
            }
        }
    }
}
